/*
*Copyright 2021-2023 NERCIS
*
*Licensed under the Apache License, Version 2.0 (the "License");
*you may not use this file except in compliance with the License.
*You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*Unless required by applicable law or agreed to in writing, software
*distributed under the License is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*See the License for the specific language governing permissions and
*limitations under the License.
*/

package cn.ac.nercis.pes.service.evaluation;

import cn.ac.nercis.pes.common.utils.ConvertUtils;
import cn.ac.nercis.pes.common.utils.PlaceholderUtils;
import cn.ac.nercis.pes.common.constant.evaluation.EvaluationConclusion;
import cn.ac.nercis.pes.common.constant.evaluation.OffsetConclusion;
import cn.ac.nercis.pes.common.constant.project.FinishState;
import cn.ac.nercis.pes.common.constant.project.RefTargetType;
import cn.ac.nercis.pes.model.dto.common.Result;
import cn.ac.nercis.pes.model.dto.common.ResultCode;
import cn.ac.nercis.pes.model.dto.evaluation.analyze.OffsetAssetDTO;
import cn.ac.nercis.pes.model.dto.evaluation.analyze.ResultCorrectionDTO;
import cn.ac.nercis.pes.model.dto.evaluation.onsite.UnitTargetEvalDTO;
import cn.ac.nercis.pes.repository.dal.evaluation.EvaluationRecordRepository;
import cn.ac.nercis.pes.repository.dal.evaluation.ResultCorrectionRepository;
import cn.ac.nercis.pes.repository.dal.evaluation.UnitRiskAnalysisRepository;
import cn.ac.nercis.pes.repository.dal.project.SystemTargetRepository;
import cn.ac.nercis.pes.repository.mapper.evaluation.ResultCorrectionMapper;
import cn.ac.nercis.pes.repository.model.evaluation.EvaluationRecordDO;
import cn.ac.nercis.pes.repository.model.evaluation.ResultCorrectionDO;
import cn.ac.nercis.pes.repository.model.project.SystemTargetDO;
import cn.ac.nercis.pes.repository.utils.DbUtils;
import cn.ac.nercis.pes.service.manage.ProcessRelatedService;
import cn.ac.nercis.pes.service.utils.AssetUtils;
import cn.ac.nercis.pes.service.utils.CalculateUtils;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 结果修正服务
 *
 * @author zpy
 * @version 1.0.0
 */
@Slf4j
@Service
public record ResultCorrectionService(ResultCorrectionRepository resultCorrectionRepository,
                                      EvaluationRecordRepository evaluationRecordRepository,
                                      SystemTargetRepository systemTargetRepository,
                                      UnitRiskAnalysisRepository riskAnalysisRepository,
                                      ProcessRelatedService processRelatedService) {
    private final static String BEFORE_RESULT_FORMAT = "%s/%s";
    private final static Double OFFSET_MAX_SCORE = 0.25;

    @Autowired
    public ResultCorrectionService {
    }

    /**
     * 修复重复数据
     */
    public void repairDuplicateData(@NonNull String systemId, @NonNull String layerTargetId) {
        var source = this.resultCorrectionRepository.findAllBySystemIdAndLayerTargetId(systemId, layerTargetId);
        var evaluationRecordIds = source.stream()
                .map(er -> er.getRecord().getId())
                .collect(Collectors.toSet());
        List<ResultCorrectionDO> duplicateData = new ArrayList<>();
        for (var recordId : evaluationRecordIds) {
            var corrections = source.stream()
                    .filter(er -> StringUtils.equals(er.getRecord().getId(), recordId))
                    .toList();
            duplicateData.addAll(DbUtils.findDuplicateData(corrections));
        }
        if (CollectionUtils.isNotEmpty(duplicateData)) {
            this.resultCorrectionRepository.deleteAll(duplicateData);
        }
    }

    public Result<List<UnitTargetEvalDTO>> findUnitTargetByLayerTarget(@NonNull String systemId, @NonNull String layerTargetId) {
        this.repairDuplicateData(systemId, layerTargetId);
        var evaluationRecords = evaluationRecordRepository.findAllBySystemIdAndLayerTargetId(systemId, layerTargetId);
        if (CollectionUtils.isEmpty(evaluationRecords)) {
            return Result.failed(ResultCode.EMPTY_EVALUATION_RECORD_BY_UNIT_TARGET);
        }
        Set<String> unitTargetIds = evaluationRecords.stream()
                .filter(er -> er.getConclusion() == EvaluationConclusion.PARTIALLY_MATCH
                        || er.getConclusion() == EvaluationConclusion.NOT_MATCH)
                .map(EvaluationRecordDO::getUnitTargetId)
                .collect(Collectors.toSet());
        if (CollectionUtils.isEmpty(unitTargetIds)) {
            return Result.failed(ResultCode.EMPTY_CORRECTION_EVALUATION_RECORD);
        }
        List<ResultCorrectionDO> resultCorrections = resultCorrectionRepository.findAllBySystemIdAndLayerTargetId(systemId, layerTargetId);
        List<UnitTargetEvalDTO> unitTargetEval = systemTargetRepository.findAllBySystemIdAndKmTargetIdIn(systemId, unitTargetIds)
                .stream()
                .sorted(Comparator.comparingInt(SystemTargetDO::getKmSort))
                .map(kt -> UnitTargetEvalDTO.builder()
                        .systemId(systemId)
                        .unitTargetId(kt.getKmTargetId())
                        .unitTargetName(kt.getKmName())
                        .finishState(getUnitTargetFinishState(kt, resultCorrections, evaluationRecords))
                        .build())
                .collect(Collectors.toList());
        return Result.success(unitTargetEval);
    }

    public Result<List<ResultCorrectionDTO>> findResultCorrectionByLayerTarget(@NonNull String systemId, @NonNull String layerTargetId, @NonNull Boolean previewMode){
        var queryUnitTargets = findUnitTargetByLayerTarget(systemId,layerTargetId);
        if(!queryUnitTargets.isSuccess()){
            return Result.failed(queryUnitTargets.getCode(), queryUnitTargets.getMsg());
        }
        return Result.success(queryUnitTargets.getData().stream()
                .flatMap(ut->findResultCorrections(systemId,ut.getUnitTargetId(),ut.getUnitTargetName(),previewMode).stream())
                .toList());
    }

    public List<ResultCorrectionDTO> findResultCorrections(@NonNull String systemId, @NonNull String unitTargetId, @NonNull Boolean previewMode) {
        return findResultCorrections(systemId,unitTargetId,"",previewMode);
    }

    public List<ResultCorrectionDTO> findResultCorrections(@NonNull String systemId, @NonNull String unitTargetId,
                                                           @NonNull String unitTargetName, @NonNull Boolean previewMode) {
        var resultCorrections = resultCorrectionRepository.findAllBySystemIdAndUnitTargetId(systemId, unitTargetId);
        return evaluationRecordRepository.findAllBySystemIdAndUnitTargetId(systemId, unitTargetId)
                .stream()
                .filter(er -> (er.getConclusion() == EvaluationConclusion.PARTIALLY_MATCH || er.getConclusion() == EvaluationConclusion.NOT_MATCH)
                        && er.getScore() <= OFFSET_MAX_SCORE)
                .map(er -> getResultCorrection(er, resultCorrections, previewMode))
                .peek(rc->rc.setUnitTargetName(unitTargetName))
                .collect(Collectors.toList());
    }

    public List<OffsetAssetDTO> findOffsetAssets(@NonNull String systemId, @NonNull String offsetAssetId, @NonNull String unitTargetId) {
        List<EvaluationRecordDO> records = this.evaluationRecordRepository.findAllBySystemId(systemId)
                .stream()
                .filter(r -> r.getRefType() == RefTargetType.STANDARD)
                .toList();
        EvaluationRecordDO offsetRecord = records.stream()
                .filter(er -> StringUtils.equals(er.getAsset().getId(), offsetAssetId)
                        && StringUtils.equals(er.getUnitTargetId(), unitTargetId))
                .findFirst().orElse(null);
        if (Objects.isNull(offsetRecord)) {
            return Collections.emptyList();
        }
        Map<String, String> targetCaches = new LinkedHashMap<>();
        return records.stream()
                .filter(er -> !StringUtils.equals(er.getAsset().getId(), offsetAssetId)
                        && Objects.nonNull(er.getScore()) && er.getScore() > offsetRecord.getScore())
                .map(er -> OffsetAssetDTO.builder()
                        .recordId(er.getId())
                        .layerTargetName(getTargetName(systemId, er.getLayerTargetId(), targetCaches))
                        .unitTargetName(getTargetName(systemId, er.getUnitTargetId(), targetCaches))
                        .assetName(AssetUtils.getAssetName(er.getAsset()))
                        .assetResult(er.getConclusion().getName())
                        .assetScore(ConvertUtils.scoreToString(er.getScore()))
                        .build())
                .collect(Collectors.toList());
    }

    public Result<OffsetAssetDTO> getOffsetAssets(@NonNull String offsetRecordId) {
        Map<String, String> targetCaches = new HashMap<>(1);
        return this.evaluationRecordRepository.findById(offsetRecordId)
                .map(er -> OffsetAssetDTO.builder()
                        .recordId(er.getId())
                        .layerTargetName(getTargetName(er.getSystemId(), er.getLayerTargetId(), targetCaches))
                        .unitTargetName(getTargetName(er.getSystemId(), er.getUnitTargetId(), targetCaches))
                        .assetName(er.getAsset().getName())
                        .assetResult(er.getConclusion().getName())
                        .assetScore(ConvertUtils.scoreToString(er.getScore()))
                        .build())
                .map(Result::success)
                .orElse(Result.failed(ResultCode.NOT_FOUND));
    }

    private String getTargetName(String systemId, String targetId, Map<String, String> targetCaches) {
        String targetName = targetCaches.getOrDefault(targetId, null);
        if (Objects.isNull(targetName)) {
            targetName = systemTargetRepository.findBySystemIdAndKmTargetId(systemId, targetId).map(SystemTargetDO::getKmName).orElse("");
            targetCaches.put(targetId, targetName);
        }
        return targetName;
    }

    public Result<ResultCorrectionDTO> computerResultCorrection(@NonNull ResultCorrectionDTO correctionDTO) {
        if (StringUtils.isNotBlank(correctionDTO.getOffsetRecordId())) {
            EvaluationRecordDO offsetRecord = this.evaluationRecordRepository.findById(correctionDTO.getOffsetRecordId())
                    .orElse(null);
            EvaluationRecordDO correctionRecord = this.evaluationRecordRepository.findById(correctionDTO.getRecordId())
                    .orElse(null);
            if (Objects.isNull(offsetRecord) || Objects.isNull(correctionRecord)) {
                return Result.failed("弥补对象测评结果记录不存在");
            }
            correctionDTO.setScore(CalculateUtils.getOffsetScore(offsetRecord.getScore(), correctionRecord.getScore()));
            correctionDTO.setResult(correctionDTO.getScore() >= 1 ? OffsetConclusion.MATCH.getName() : OffsetConclusion.PARTIALLY_MATCH.getName());
        } else {
            correctionDTO.setScore(null);
            correctionDTO.setResult("");
        }
        return Result.success(correctionDTO);
    }

    public Result<ResultCorrectionDTO> saveResultCorrection(@NonNull ResultCorrectionDTO correctionDTO) {
        Result<ResultCorrectionDTO> computerResult = computerResultCorrection(correctionDTO);
        if (!computerResult.isSuccess()) {
            return computerResult;
        }
        if (StringUtils.isBlank(correctionDTO.getId())) {
            return createResultCorrection(correctionDTO);
        } else {
            return modifyResultCorrection(correctionDTO);
        }
    }

    public Result<String> batchSaveResultCorrection(List<ResultCorrectionDTO> data) {
        Result<String> result = Result.success();
        for (ResultCorrectionDTO item : data) {
            Result<ResultCorrectionDTO> itemResult = saveResultCorrection(item);
            if (!itemResult.isSuccess()) {
                result = Result.failed(ResultCode.FAIL);
                break;
            }
        }
        return result;
    }

    public Result<String> setFinishStateByLayerTarget(@NonNull String systemId, @NonNull String layerTargetId, @NonNull FinishState state) {
        var resultCorrections = resultCorrectionRepository.findAllBySystemIdAndLayerTargetId(systemId, layerTargetId)
                .stream()
                .peek(rc->{
                    rc.setFinishState(state);
                    rc.setModifyTime(LocalDateTime.now());
                })
                .toList();
        if (CollectionUtils.isEmpty(resultCorrections)) {
            return Result.failed(ResultCode.CORRECTION_EVALUATION_RECORD_NOT_EXIST);
        }
        resultCorrectionRepository.saveAll(resultCorrections);
        return Result.success(layerTargetId);
    }

    private Result<ResultCorrectionDTO> createResultCorrection(ResultCorrectionDTO evaluationDTO) {
        EvaluationRecordDO recordDO = evaluationRecordRepository.findById(evaluationDTO.getRecordId()).orElse(null);
        if (Objects.isNull(recordDO)) {
            return Result.failed(ResultCode.EVALUATION_RECORD_NOT_EXIST);
        }
        return Optional.of(ResultCorrectionMapper.INSTANCE.toDO(evaluationDTO))
                .map(er -> {
                    er.setScore(evaluationDTO.getScore());
                    er.setFinishState(FinishState.INCOMPLETE);
                    er.setRecord(recordDO);
                    er.setCreateTime(LocalDateTime.now());
                    return er;
                })
                .map(resultCorrectionRepository::save)
                .map(entity -> {
                    ResultCorrectionDTO dto = ResultCorrectionMapper.INSTANCE.toDTO(entity);
                    dto.setAssetName(recordDO.getAsset().getName());
                    dto.setBeforeResult(String.format(BEFORE_RESULT_FORMAT,
                            recordDO.getConclusion().getName(),
                            ConvertUtils.scoreToString(recordDO.getScore(), "")));
                    return dto;
                })
                .map(Result::success)
                .orElse(Result.failed());
    }

    private Result<ResultCorrectionDTO> modifyResultCorrection(ResultCorrectionDTO evaluationDTO) {
        ResultCorrectionDO entity = resultCorrectionRepository.findById(evaluationDTO.getId()).orElse(null);
        if (Objects.isNull(entity)) {
            return Result.failed(ResultCode.CORRECTION_EVALUATION_RECORD_NOT_EXIST);
        }
        OffsetConclusion conclusion = OffsetConclusion.fromName(evaluationDTO.getResult());
        if (entity.getResult() != conclusion) {
            entity.setResult(conclusion);
            processRelatedService.deleteResultCorrectionRelatedData(entity);
            processRelatedService.resetResultCorrectionRelatedModuleProcess(entity);
        }
        entity.setOffsetRecordId(evaluationDTO.getOffsetRecordId());
        entity.setScore(evaluationDTO.getScore());
        entity.setDesc(evaluationDTO.getDesc());
        entity.setModifyTime(LocalDateTime.now());
        return Optional.of(entity)
                .map(resultCorrectionRepository::save)
                .map(correctionDO -> {
                    ResultCorrectionDTO dto = ResultCorrectionMapper.INSTANCE.toDTO(correctionDO);
                    dto.setAssetName(correctionDO.getRecord().getAsset().getName());
                    dto.setBeforeResult(String.format(BEFORE_RESULT_FORMAT,
                            correctionDO.getRecord().getConclusion().getName(),
                            ConvertUtils.scoreToString(correctionDO.getRecord().getScore(), "")));
                    return dto;
                })
                .map(Result::success)
                .orElse(Result.failed());
    }

    private ResultCorrectionDTO getResultCorrection(EvaluationRecordDO recordDO, List<ResultCorrectionDO> resultCorrections, Boolean previewMode) {
        Optional<ResultCorrectionDO> correctionDO = resultCorrections.stream()
                .filter(rc -> StringUtils.equals(rc.getRecord().getId(), recordDO.getId()))
                .findFirst();
        return correctionDO.map(ResultCorrectionMapper.INSTANCE::toDTO)
                .map(rc -> {
                    rc.setAssetId(recordDO.getAsset().getId());
                    rc.setAssetName(AssetUtils.getAssetName(recordDO.getAsset()));
                    rc.setBeforeResult(String.format(BEFORE_RESULT_FORMAT,
                            recordDO.getConclusion().getName(),
                            ConvertUtils.scoreToString(recordDO.getScore(), "")));
                    if (StringUtils.isNotBlank(rc.getOffsetRecordId())) {
                        evaluationRecordRepository.findById(rc.getOffsetRecordId())
                                .ifPresent(or -> {
                                    rc.setOffsetAssetName(AssetUtils.getAssetName(or.getAsset()));
                                    rc.setOffsetScore(or.getScore());
                                    if (previewMode && StringUtils.isNotBlank(rc.getDesc())) {
                                        String previewContent = PlaceholderUtils.builder()
                                                .withAssetName(AssetUtils.getAssetName(or.getAsset()))
                                                .withAssetScore(ConvertUtils.scoreToString(or.getScore(), ""))
                                                .build()
                                                .renderContent(rc.getDesc());
                                        rc.setDesc(previewContent);
                                    }
                                });
                    }
                    return rc;
                })
                .orElse(ResultCorrectionDTO.builder()
                        .systemId(recordDO.getSystemId())
                        .layerTargetId(recordDO.getLayerTargetId())
                        .unitTargetId(recordDO.getUnitTargetId())
                        .recordId(recordDO.getId())
                        .assetId(recordDO.getAsset().getId())
                        .assetName(AssetUtils.getAssetName(recordDO.getAsset()))
                        .beforeResult(String.format(BEFORE_RESULT_FORMAT,
                                recordDO.getConclusion().getName(),
                                ConvertUtils.scoreToString(recordDO.getScore())))
                        .result(OffsetConclusion.DEFAULT.getName())
                        .build());
    }

    private FinishState getUnitTargetFinishState(SystemTargetDO targetDO, List<ResultCorrectionDO> resultCorrections,
                                                 List<EvaluationRecordDO> evaluationRecords) {
        var unitResultCorrections = resultCorrections.stream()
                .filter(rc -> StringUtils.equals(rc.getUnitTargetId(), targetDO.getKmTargetId()))
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(unitResultCorrections)) {
            return FinishState.NONE;
        }
        long count = evaluationRecords.stream()
                .filter(er -> StringUtils.equals(er.getUnitTargetId(), targetDO.getKmTargetId()))
                .filter(er -> er.getConclusion() == EvaluationConclusion.PARTIALLY_MATCH
                        || er.getConclusion() == EvaluationConclusion.NOT_MATCH)
                .count();
        long finishCount = unitResultCorrections.stream()
                .filter(rc -> rc.getFinishState() == FinishState.FINISH)
                .count();
        return count == finishCount ? FinishState.FINISH : FinishState.INCOMPLETE;
    }
}